﻿/* 关于 IB, 您可以参考 http://blog.sina.com.cn/s/blog_604c436d0100o5j0.html.
 * IB 是项目 zsharedcode 的一部分, 您可以参考 http://code.google.com/p/zsharedcode/ 或者 http://blog.sina.com.cn/s/blog_604c436d0100o04s.html.
 * Visual Studio 2010 项目模板请访问 http://blog.sina.com.cn/s/blog_604c436d0100o8gn.html.
 * Bug 请发送至: m.s.cxc@qq.com.
 * 所有项目源码托管地址: http://panzer.codeplex.com/.
 * Visual Studio 库地址: http://visualstudiogallery.msdn.microsoft.com/8626c1ed-8523-4247-bd79-087b9a84d653.
 * */
#define USELOCALSETTING

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.Xml;

using zoyobar.shared.panzer;
using zoyobar.shared.panzer.web;
using zoyobar.shared.panzer.web.ib;
using zoyobar.shared.panzer.flow;

// HACK: 如运行有偏差，代码中的 jQuery 可能需要依据最新的 Google 或者 Bing 页面， IE 或者 jQuery 版本调整。或者注释掉 #define USELOCALSETTING，采用网络设置中的 jQuery。

namespace IBESTemplate
{

	public partial class FormTemplate : Form
	{

#if !USELOCALSETTING
		// 从 http://zoyobar.googlecode.com/files/IBESTemplate.setting.xml  载入的设置。
		private readonly SortedList<string, string> settings = new SortedList<string, string> ( );
#endif

		// 载入 Google 首页的页面状态名称。
		private const string loadGoogleStateName = "load google";
		// 在 Google 首页安装 jQuery 的页面状态名称。
		private const string homePageInstallJQueryStateName = "home page install jquery";
		// 测试 Google 首页是否成功载入 jQuery 的页面状态名称。
		private const string testJQueryStateName = "test jquery";
		// 载入 Google 结果的页面状态名称。
		private const string searchStateName = "search";
		// 在 Google 结果页面安装 jQuery 的页面状态名称。
		private const string resultPageInstallJQueryStateName = "result page install jquery";
		// 载入 Google 下一页结果的页面状态名称。
		private const string nextPageStateName = "next page";
		// TODO: 可以在此处添加新的或者修改上面的页面状态名称。

		// 是否停止当前的搜索。
		private bool isStop;
		// 当前 Google 结果页面的页码。
		private int currentPageIndex;
		// 保存 Google 上搜索结果的地址和标题。
		private readonly SortedList<string, string> results = new SortedList<string, string> ( );
		// IEBrowser，完成操作的核心类。
		private readonly IEBrowser ie;

		// 载入 Google 首页的页面状态。
		private readonly WebPageState loadGoogleState = new WebPageState (
			loadGoogleStateName,
			// TODO: 在此处修改自己的搜索引擎的地址，比如："http://cn.bing.com"。
			new NavigateAction ( "http://www.google.com.hk", 2 ),
			new WebPageNextStateSetting (
				homePageInstallJQueryStateName, // homePageInstallJQueryStateName 表示载入 Google 首页后，进入在首页安装 jQuery 的状态。
				true // true 表示载入 Google 首页后，自动进入下一状态。
			),
			//	TODO: 在此处可以修改为不同的地址，比如："http://cn.bing.com"。
			new UrlCondition ( "url google", "http://www.google.com.hk", StringCompareMode.StartWith ) // 这里表示当浏览器载入页面的地址以 "http://www.google.com.hk" 开头时，表示载入 Google 首页的页面状态已经完成，并可以进入下一状态。
			);

		// 在 Google 首页安装 jQuery 的页面状态。
		private readonly WebPageState homePageInstallJQueryState = new WebPageState (
			homePageInstallJQueryStateName,
			//	TODO: 在此处修改为自定义的 jQuery 脚本下载地址。之前使用载入本地的 jquery 脚本由于编码问题出现错误，现使用另外的方式加载。
			//	new InstallJQueryAction ( Path.Combine ( AppDomain.CurrentDomain.BaseDirectory, @"jquery-1.5.min.js" ), 1 ), // 这里表示从本地载入 jQuery 脚本，并在载入后等待 1 秒钟。
			new WebPageAction[] {
				new InstallTraceAction ( ), // 这里安装跟踪脚本，是 jQuery 所需的脚本。
				new InstallJavaScriptAction ( Properties.Resources.jquery_1_5_min, "jsJQuery_1_5" ) // 这里安装 jQuery 1.5 。
			},
			new WebPageNextStateSetting (
				testJQueryStateName, // testJQueryStateName 表示 jQuery 载入后，进入测试 jQuery 的状态。
				true // true 表示载入  jQuery 载入后，自动进入下一状态。
			)
			);

		// 测试 Google 首页是否成功载入 jQuery 的页面状态。
		private readonly WebPageState testJQueryState = new WebPageState (
			testJQueryStateName,
			// TODO: 在此处可以修改自定义的测试脚本，也可以用 null 或者 string.Empty 代替脚本。
			new ExecuteJavaScriptAction ( "try{alert('jQuery 已经载入, 页面上共有 ' + $('*').length + ' 个元素');}catch(err){alert('jQuery 脚本尚未成功载入, 请重新开始!');}" ), // 这里表示在浏览器中运行上面的 javascript 脚本，其中 $('*').length 是需要 jQuery 载入成功才能正常运行的。
			new WebPageNextStateSetting (
				searchStateName, // searchStateName 表示测试 jQuery 后，进入搜索的状态。
				true // true 表示测试 jQuery 后，自动进入下一状态。
			)
			);

		// 载入 Google 结果的页面状态。
		private readonly WebPageState searchState = new WebPageState (
			searchStateName,
			new WebPageAction[] {
				new ExecuteJQueryAction ( JQuery.Create ( ) ), // 这里的 jQuery 是预留的用于设置搜索关键字的，在运行时将被设置。
				// TODO: 在此处根据自己的搜索页面来设置点击搜索按钮的 jQuery 命令，以 http://cn.bing.com/ 为例，需要改为： JQuery.Create ( "'#sb_form_go'" ).Trigger ( "'click'" ) 。
				new ExecuteJQueryAction ( JQuery.Create ( "'input[name=btnG]'" ).Trigger ( "'click'" ) ) // 这里表示的 jQuery 命令表示点击 Google 页面上的搜索按钮。
			},
			new WebPageNextStateSetting (
				resultPageInstallJQueryStateName, // resultPageInstallJQueryStateName 载入 Google 结果，进入在结果页面安装 jQuery 的状态。
				true // true 表示载入 Google 结果，自动进入下一状态。
			),
			//	TODO: 在此处可以修改为不同的地址，比如："http://cn.bing.com/search"。
			new UrlCondition ( "url search", "http://www.google.com.hk/search", StringCompareMode.StartWith ) // 这里表示当浏览器载入页面的地址以 "http://www.google.com.hk/search" 开头时，表示载入 Google 结果的页面状态已经完成，并可以进入下一状态。
			);

		// 在 Google 结果页面安装 jQuery 的页面状态。
		private readonly WebPageState resultPageInstallJQueryState = new WebPageState (
			resultPageInstallJQueryStateName,
			//	TODO: 在此处修改为自定义的 jQuery 脚本下载地址。之前使用载入本地的 jquery 脚本由于编码问题出现错误，现使用另外的方式加载。
			// new InstallJQueryAction ( Path.Combine ( AppDomain.CurrentDomain.BaseDirectory, @"jquery-1.5.min.js" ), 1 ), // 这里表示从本地载入 jQuery 脚本，并在载入后等待 1 秒钟。
			new WebPageAction[] {
				new InstallTraceAction ( ), // 这里安装跟踪脚本，是 jQuery 所需的脚本。
				new InstallJavaScriptAction ( Properties.Resources.jquery_1_5_min, "jsJQuery_1_5" ) // 这里安装 jQuery 1.5 。
			},
			new WebPageNextStateSetting (
				nextPageStateName, // nextPageStateName 表示 jQuery 载入后，进入载入 Google 下一页结果的页面状态。
				true // true 表示 jQuery 载入后，自动进入下一状态。
			)
			);

		// 载入 Google 下一页结果的页面状态。
		private readonly WebPageState nextPageState = new WebPageState (
			nextPageStateName,
			new ExecuteJavaScriptAction ( null ), // 用于跳转到下一页的 javascript，之后将被设置。
			new WebPageNextStateSetting (
				resultPageInstallJQueryStateName, // resultPageInstallJQueryStateName 表示载入 Google 下一页结果，进入在结果页面安装 jQuery 的状态。
				true // true 表示载入 Google 下一页结果，自动进入下一状态。
			),
			//	TODO: 在此处可以修改为不同的地址，比如："http://cn.bing.com/search"。
			new UrlCondition ( "url search", "http://www.google.com.hk/search", StringCompareMode.StartWith ) // 这里表示当浏览器载入页面的地址以 "http://www.google.com.hk/search" 开头时，表示载入 Google 结果的页面状态已经完成，并可以进入下一状态。
			);
		// TODO: 可以在此处添加新的或者修改上面的页面状态。

		public FormTemplate ( )
		{
			InitializeComponent ( );

#if !USELOCALSETTING
			#region " 使用远程设置时，重新设置页面状态。 "
			try
			{
				// 获取 http://zoyobar.googlecode.com/files/IBESTemplate.setting.xml 中包含的设置。
				XmlDocument xml = new XmlDocument ( );
				xml.Load ( "http://zoyobar.googlecode.com/files/IBESTemplate.setting.xml " );

				// 这里省去了检查设置。
				foreach ( XmlNode node in xml.SelectNodes ( "//setting" ) )
					this.settings.Add ( node.Attributes["name"].Value, node.Attributes["value"].Value );

				// 根据设置重新定义载入 Google 首页的页面状态。
				this.loadGoogleState = new WebPageState (
					loadGoogleStateName,
					new NavigateAction ( this.settings["HomePage"], 2 ),
					new WebPageNextStateSetting (
						homePageInstallJQueryStateName,
						true
					),
					new UrlCondition ( "url google", this.settings["HomePage"], StringCompareMode.StartWith )
					);

				// 根据设置重新定义载入 Google 结果的页面状态的点击搜索按钮的 jQuery 命令。
				this.searchState = new WebPageState (
					searchStateName,
					new WebPageAction[] {
						new ExecuteJQueryAction ( JQuery.Create ( ) ),
						new ExecuteJQueryAction ( JQuery.Create ( this.settings["SearchButton"] ).Trigger ( "'click'" ) )
					},
					new WebPageNextStateSetting (
						resultPageInstallJQueryStateName,
						true
					),
					new UrlCondition ( "url search", this.settings["ResultPage"], StringCompareMode.StartWith )
					);

				// 根据设置重新定义载入 Google 下一页结果的页面状态。
				this.nextPageState = new WebPageState (
					nextPageStateName,
					new ExecuteJavaScriptAction ( null ),
					new WebPageNextStateSetting (
						resultPageInstallJQueryStateName,
						true
					),
					new UrlCondition ( "url search", this.settings["ResultPage"], StringCompareMode.StartWith )
					);

			}
			catch { }
			#endregion
#endif

			// 初始化 IEBrowser，将 WebBrower 控件和页面状态传递给 IEBrowser 。
			this.ie = new IEBrowser ( this.webBrowser, new WebPageState[] {
					this.loadGoogleState, this.homePageInstallJQueryState, this.testJQueryState, this.searchState,
					this.resultPageInstallJQueryState, this.nextPageState
				}
				);

			// 当浏览器载入一个符合条件的地址时。
			this.ie.IEFlow.ConditionCompleted += new ConditionCompletedEventHandler<WebPageCondition> ( this.ieConditionCompleted );
			// 当剩余等待时间改变时。
			this.ie.IEFlow.RemainWaitTimeChanged += new RemainWaitTimeChangedEventHandler ( this.ieRemainWaitTimeChanged );

			// 当页面状态改变时。
			this.ie.IEFlow.StateCompleted += new StateChangedEventHandler<WebPageAction, WebPageCondition> ( this.ieStateCompleted );

		}

		private void ieConditionCompleted ( WebPageCondition condition, int conditionCount, int completedConditionCount )
		{ this.lblInfo.Text = string.Format ( "条件 {0} ({1}/{2}) 完成 ...", condition.Name, completedConditionCount, conditionCount ); }

		private void ieRemainWaitTimeChanged ( int remainSecond )
		{

			if ( remainSecond <= 0 )
				this.lblTime.Text = "...";
			else
				this.lblTime.Text = string.Format ( "等待 {0} 秒 ...", remainSecond );

		}

		private void ieStateCompleted ( FlowState<WebPageAction, WebPageCondition> state, ref bool isStop )
		{

			//	是否被用户停止，如果用户停止，则停止页面状态的转换，也就是停止搜索。
			if ( this.isStop )
			{
				isStop = true;
				return;
			}

			Console.WriteLine ( state.Name );

			switch ( state.Name )
			{
				case resultPageInstallJQueryStateName:
					// 如果完成的页面状态为载入 Google 结果页面，那么需要获取搜索的结果。

#if USELOCALSETTING
					// TODO: 在此处根据不同的搜索引擎调整，以 http://cn.bing.com/ 为例，可以修改为： JQuery.Create ( "'div.sa_cc'" ) 。
					this.ie.ExecuteJQuery ( JQuery.Create ( "'li.g'" ), "__jlis" ); // 获取页面上所有样式包含 g 的 li ，并将结果保存在页面的 __jlis 变量中。

					int resultCount = this.ie.ExecuteJQuery<int> ( JQuery.Create ( "__jlis" ).Length ( ), "__jfSearch" ); // 获取页面上搜索到页面的个数，即获取 __jlis 变量的 length 属性。

					for ( int index = 0; index < resultCount; index++ )
					{
						this.ie.ExecuteJQuery ( JQuery.Create ( "__jlis" ).Eq ( index.ToString ( ) ), "__jli" ); // 获取单个的 li ，并将结果保存在页面的 __jli 变量中。

						// TODO: 在此处根据不同的搜索引擎调整，以 http://cn.bing.com/ 为例，可以修改为： JQuery.Create ( "__jli" ).Children ( "'.sb_tlst'" ).Text ( ) 。
						string title = this.ie.ExecuteJQuery<string> ( JQuery.Create ( "__jli" ).Find ( "'.r'" ).Text ( ), "__jfSearch" ); // 获取 li 中样式包含 r 的元素的 innerText 属性，就是搜索结果的标题。

						// TODO: 在此处根据不同的搜索引擎调整，以 http://cn.bing.com/ 为例，可以修改为： JQuery.Create ( "__jli" ).Find ( "'a'" ).Attr ( "'href'" ) 。
						string url = this.ie.ExecuteJQuery<string> ( JQuery.Create ( "__jli" ).Find ( "'a.l'" ).Attr ( "'href'" ), "__jfSearch" ); // 获取 li 中样式包含 l 的 a 元素的 href 属性，就是搜索结果的地址。

						// 如果这个搜索结果有效，保存在 results 中，并显示在界面中。
						if ( !string.IsNullOrEmpty ( title ) && !string.IsNullOrEmpty ( url ) && !this.results.ContainsKey ( url ) )
						{
							this.listPage.Items.Add ( title + "-" + url );
							this.results.Add ( url, title );
						}

					}

					// TODO: 在此处根据不同的搜索引擎调整，以 http://cn.bing.com/ 为例，可以修改为： JQuery.Create ( "'a.sb_pagN'" ).Attr ( "'href'" ) 。
					( this.nextPageState.StartActions[0] as ExecuteJavaScriptAction ).Code = string.Format ( "window.location = '{0}';", this.ie.ExecuteJQuery<string> ( JQuery.Create ( "'#pnnext'" ).Attr ( "'href'" ), "__jfSearch" ) ); // 获取 Google 页面上 id 为 pnnext 的元素的 href 属性，也就是下一页的地址，并设置为跳转到下一页动作的 javascript 代码。
#else
					#region " 使用远程设置时的代码。 "

					this.ie.ExecuteJQuery ( JQuery.Create ( this.settings["ResultItem"] ), "__jlis" );

					int resultCount = this.ie.ExecuteJQuery<int> ( JQuery.Create ( "__jlis" ).Length ( ), "__jfSearch" );

					for ( int index = 0; index < resultCount; index++ )
					{
						this.ie.ExecuteJQuery ( JQuery.Create ( "__jlis" ).Eq ( index.ToString ( ) ), "__jli" );

						string title = this.ie.ExecuteJQuery<string> ( JQuery.Create ( "__jli" ).Children ( this.settings["ResultTitle"] ).Text ( ), "__jfSearch" );

						string url = this.ie.ExecuteJQuery<string> ( JQuery.Create ( "__jli" ).Find ( this.settings["ResultUrl"] ).Attr ( "'href'" ), "__jfSearch" );

						if ( !string.IsNullOrEmpty ( title ) && !string.IsNullOrEmpty ( url ) && !this.results.ContainsKey ( url ) )
						{
							this.listPage.Items.Add ( title + "-" + url );
							this.results.Add ( url, title );
						}

					}

					( this.nextPageState.StartActions[0] as ExecuteJavaScriptAction ).Code = string.Format ( "window.location = '{0}';", this.ie.ExecuteJQuery<string> ( JQuery.Create ( this.settings["NextButton"] ).Attr ( "'href'" ), "__jfSearch" ) );
					#endregion
#endif

					// 当前的 Google 结果页面的页码已经是用户指定的最大页码，则停止搜索。
					if ( this.currentPageIndex++ >= this.numericPageCount.Value )
					{
						this.stopSearch ( );
						isStop = true;
					}

					break;
			}

		}

		private void webClientOpenReadCompleted ( object sender, OpenReadCompletedEventArgs e )
		{
			// 将采集到的页面保存到对应的文件夹中。

			if ( null != e.Error )
				return;

			// HACK: 可能因为网络原因导致有些页面在此处抛出异常。
			try
			{ File.WriteAllText ( e.UserState.ToString ( ), new StreamReader ( e.Result, Encoding.Default ).ReadToEnd ( ), Encoding.Default ); }
			catch { }

			e.Result.Close ( );
		}

		private void stopSearch ( )
		{
			this.ie.IEFlow.StopJump ( );
			this.isStop = true;

			this.cmdSearch.Enabled = true;
			this.cmdStop.Enabled = false;
			this.cmdSpider.Enabled = true;

			this.txtKeyWord.Enabled = true;
			this.numericPageCount.Enabled = true;
			this.lblInfo.Text = "...";
			this.lblTime.Text = "...";
		}

		private void cmdSearch_Click ( object sender, EventArgs e )
		{
			string keyWord = this.txtKeyWord.Text.Trim ( );

			if ( keyWord == string.Empty )
			{
				this.lblInfo.Text = "请填写关键字";
				return;
			}

			this.isStop = false;

			this.cmdSearch.Enabled = false;
			this.cmdStop.Enabled = true;
			this.cmdSpider.Enabled = false;

			this.txtKeyWord.Enabled = false;
			this.numericPageCount.Enabled = false;

			//	清除上一次搜索的结果。
			this.results.Clear ( );
			this.listPage.Items.Clear ( );

			this.currentPageIndex = 1;

#if USELOCALSETTING
			// TODO: 在此处根据不同的搜索引擎调整，以 http://cn.bing.com/ 为例，可以修改为： JQuery.Create ( "'#sb_form_q'" ).Val ( string.Format ( "'{0}'", keyWord ) ) 。
			( this.searchState.StartActions[0] as ExecuteJQueryAction ).JQuery = JQuery.Create ( "'input[name=q]'" ).Val ( string.Format ( "'{0}'", keyWord ) ); // 这里根据用户输入的关键字，生成设置 Google 首页搜索框文本的 jQuery 。
#else
			( this.searchState.StartActions[0] as ExecuteJQueryAction ).JQuery = JQuery.Create ( this.settings["SearchTextBox"] ).Val ( string.Format ( "'{0}'", keyWord ) );
#endif

			// 从第一个状态开始，载入 Google 首页。
			this.ie.IEFlow.JumpToState ( loadGoogleStateName );
		}

		private void cmdStop_Click ( object sender, EventArgs e )
		{ this.stopSearch ( ); }

		private void cmdSpider_Click ( object sender, EventArgs e )
		{

			if ( this.folderBrowserDialog.ShowDialog ( ) == DialogResult.Cancel )
				return;

			// 对搜索到的结果异步载入。

			foreach ( string url in this.results.Keys )
				try
				{
					this.lblInfo.Text = string.Format ( "采集 {0}", url );
					this.status.Refresh ( );

					WebClient client = new WebClient ( );
					client.Encoding = Encoding.Default;

					client.OpenReadCompleted += new OpenReadCompletedEventHandler ( this.webClientOpenReadCompleted ); // 设置载入完成的事件，在此事件中保存页面到文件夹。

					client.OpenReadAsync ( new Uri ( url ), Path.Combine ( this.folderBrowserDialog.SelectedPath, this.results[url] + ".htm" ) ); // 这里同时，传递了页面保存的路径，供 webClientOpenReadCompleted 方法使用。
				}
				catch { }

			this.lblInfo.Text = "采集命令发送完成, 请稍后到文件夹查看结果.";
		}

	}

}
